/**
 * @author Francesco Rosso, matr 592057
 */

package pcd1112.model;

import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Vector;

import pcd1112.model.eccezioni.ErrorException;
import pcd1112.model.eccezioni.NodoPresenteException;

public class Hypervisor {

	/**
	 * This class calculates the min, max, average an Standard Deviation number
	 * of the parameters set
	 * 
	 * @author R2D2
	 * 
	 */
	private class MinMaxAvgDev {
		private Vector<Integer> values = new Vector<Integer>();

		private int min = 0, max = 0;
		private double stdDev = 0;

		private double avr = 0;

		/**
		 * Adds a value to the parameters
		 * 
		 * @param value
		 */
		public void addValue(int value) {
			values.add(value);
		}

		private double stdDev(double sum, double sum_2) {
			return Math.sqrt((sum_2 - (sum * sum / values.size()))
					/ (values.size() - 1));

		}

		/**
		 * Compute everything
		 */
		public void compute() {
			// min, max, average, standard deviation of sent messages (s)
			int min = (int) values.get(0);
			int max = min;
			double sum_s = max; // sum of the xi
			double sum_sq = max * max; // sum of the xi^2
			for (int i = 1; i < values.size(); i++) {
				int calc = (int) values.get(i);
				if (calc < min)
					min = calc;
				else if (calc > max)
					max = calc;
				sum_s += calc;
				sum_sq += (calc * calc);
			}
			double avr = sum_s / (values.size());
			double stand_devs = stdDev(sum_s, sum_sq);

			double avr_approx = new BigDecimal(avr).setScale(4,
					BigDecimal.ROUND_UP).doubleValue(); // 4 digits approx
			double stand_dev_approx = new BigDecimal(stand_devs).setScale(4,
					BigDecimal.ROUND_UP).doubleValue();

			setMin(min);
			setMax(max);
			setAvr(avr_approx);
			setStdDev(stand_dev_approx);
		}

		/**
		 * PRE: compute() needs to be called, first
		 * 
		 * @return the max
		 */
		public int getMax() {
			return max;
		}

		/**
		 * @param max
		 *            the max to set
		 */
		private void setMax(int max) {
			this.max = max;
		}

		/**
		 * PRE: compute() needs to be called, first
		 * 
		 * @return the min
		 */
		public int getMin() {
			return min;
		}

		/**
		 * @param min
		 *            the min to set
		 */
		private void setMin(int min) {
			this.min = min;
		}

		/**
		 * PRE: compute() needs to be called, first
		 * 
		 * @return the avr
		 */
		public double getAvr() {
			return avr;
		}

		/**
		 * @param avr
		 *            the avr to set
		 */
		private void setAvr(double avr) {
			this.avr = avr;
		}

		/**
		 * PRE: compute() needs to be called, first
		 * 
		 * @return the Standard Deviation
		 */
		public double getStdDev() {
			return stdDev;
		}

		/**
		 * @param stdDev
		 *            the stdDev to set
		 */
		private void setStdDev(double stdDev) {
			this.stdDev = stdDev;
		}
	}

	private MinMaxAvgDev sentM, receivM, signatureChecks, messagesStored,
			consumedEnergy;
	private String serverUrl = "localhost";

	/**
	 * Initialize the unit square area (without the clone) with the number of
	 * nodes specified in the configuration. PRE: Config must have been
	 * initialized POST: inut square area initialized
	 */
	private void initArea() {
		Ambiente a = Ambiente.getAmbiente();
		a.reset();
		Coordinata coord;
		Nodo nod;
		for (int i = 0; i < Config.getConfig().getN(); i++) {
			try {
				coord = new Coordinata(Math.random(), Math.random());
				nod = new Nodo(coord, i, Config.getConfig().getE());
				a.aggiungiNodo(nod);
			} catch (NodoPresenteException e) {
				i--;
			}

		}

		if (Config.getConfig().getCurrentMode() == Config.Mode.real) {
			AwaitStop.await(a.getNodi());
		}
	}

	/**
	 * This method insert the clone in the unit square area using a random ID.
	 * The ID is choosen between one of the nodes created before. PRE:
	 * initArea() has to be called before POST: there's a clone in the area
	 */
	private void insertClone() {
		Ambiente a = Ambiente.getAmbiente();
		Coordinata coord;
		Nodo nod;
		// let's try to find a node to clone
		ArrayList<INodo> nodi = a.getNodi();
		INodo nodoCopia = nodi.get((int) ((Math.random()) * nodi.size()));
		int id = nodoCopia.getIdNodo();
		nodoCopia.setCloned(true);

		boolean ok = false;
		while (!ok) {
			try {
				coord = new Coordinata(Math.random(), Math.random());
				nod = new Nodo(coord, id, Config.getConfig().getE());
				nod.setClone(true);
				a.aggiungiNodo(nod);
				ok = true;
			} catch (NodoPresenteException e) {
				// esiste già un nodo in questa posizione
			}
		}

		if (Config.getConfig().getCurrentMode() == Config.Mode.real) {
			AwaitStop.await(a.getNodi());
		}
	}

	/**
	 * Init the unit-square area with the clone PRE: config must have been
	 * initialized
	 */
	public void init() {
		initArea();
		insertClone();
	}

	/**
	 * Starts one of the NSIM simulations PRE: init() has to be called before
	 * POST: the simulation is terminated and the stats have been calculated
	 */
	public void startSimulation() {
		Ambiente a = Ambiente.getAmbiente();
		a.startSimulation();
		AwaitStop.await(a.getNodi());
		stats();
	}

	/**
	 * Calculate the statistics about the last simulation PRE: startSimulation()
	 * has to be called before
	 */
	private void stats() {
		sentM = new MinMaxAvgDev();
		receivM = new MinMaxAvgDev();
		signatureChecks = new MinMaxAvgDev();
		messagesStored = new MinMaxAvgDev();
		consumedEnergy = new MinMaxAvgDev();
		DescrizioneNodo d;
		Ambiente a = Ambiente.getAmbiente();

		for (int i = 0; i < a.getNodi().size(); i++) {
			d = a.getNodi().get(i).getStato();
			sentM.addValue(d.getNumMessaggiSpediti());
			receivM.addValue(d.getNumMessaggiRicevuti());
			signatureChecks.addValue(d.getNumSignatureChecks());
			messagesStored.addValue(d.getNumMessaggiMemorizzati());
			consumedEnergy.addValue(Config.getConfig().getE()
					- d.getEnergiaRimasta());
		}
		sentM.compute();
		receivM.compute();
		signatureChecks.compute();
		messagesStored.compute();
		consumedEnergy.compute();
	}

	/**
	 * @return the serverUrl
	 */
	public String getServerUrl() {
		return serverUrl;
	}

	/**
	 * @param serverUrl
	 *            the serverUrl to set
	 */
	public void setServerUrl(String serverUrl) {
		this.serverUrl = serverUrl;
	}

	/**
	 * Sends the result to the specified rmi server
	 * 
	 * @return the sent string
	 * @throws ErrorException
	 *             if there is a connection error
	 */
	public String sendResults() throws ErrorException {
		String output = "";
		try {
			Config c = Config.getConfig();
			TxtWriter ref = (TxtWriter) Naming.lookup("rmi://" + getServerUrl()
					+ "/Writer");
			output = c.getPROTO() + " " + c.getNSIM() + " " + c.getN() + " "
					+ c.getR() + " " + c.getP() + " " + c.getG() + " "
					+ c.getE() + " " + c.getE_send() + " " + c.getE_receive()
					+ " " + c.getE_signature() + " ";
			output += sentM.getMin() + " " + sentM.getMax() + " "
					+ sentM.getAvr() + " " + sentM.getStdDev() + " ";
			output += receivM.getMin() + " " + receivM.getMax() + " "
					+ receivM.getAvr() + " " + receivM.getStdDev() + " ";
			output += signatureChecks.getMin() + " " + signatureChecks.getMax()
					+ " " + signatureChecks.getAvr() + " "
					+ signatureChecks.getStdDev() + " ";
			output += consumedEnergy.getMin() + " " + consumedEnergy.getMax()
					+ " " + consumedEnergy.getAvr() + " "
					+ consumedEnergy.getStdDev() + " ";
			output += messagesStored.getMin() + " " + messagesStored.getMax()
					+ " " + messagesStored.getAvr() + " "
					+ messagesStored.getStdDev() + " ";
			output += Ambiente.getAmbiente().isCloneFound() ? "1" : "0";
			ref.append(output);
			return output;
		} catch (MalformedURLException e) {
			throw new ErrorException("Connection error", e.getMessage());
		} catch (RemoteException e) {
			throw new ErrorException("Connection error", e.getMessage());
		} catch (NotBoundException e) {
			throw new ErrorException("Connection error", e.getMessage());
		}
	}

}
